package cn.coldleaf.wiki.filters;

import cn.coldleaf.wiki.configure.AppConfig;
import cn.coldleaf.wiki.utils.ByteBuffer;
import cn.coldleaf.wiki.utils.JsonUtil;
import cn.coldleaf.wiki.utils.StringUtil;
import cn.coldleaf.wiki.utils.TokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.util.ParameterMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * token 转 userId 过滤器
 * @datetime 2023/10/21 下午10:38
 * @user cold
 **/
@Slf4j
@Component
public class TokenFilter implements Filter {
    @Autowired
    private AppConfig appConfig;
    /**
     * The <code>doFilter</code> method of the Filter is called by the container
     * each time a request/response pair is passed through the chain due to a
     * client request for a resource at the end of the chain. The FilterChain
     * passed in to this method allows the Filter to pass on the request and
     * response to the next entity in the chain.
     * <p>
     * A typical implementation of this method would follow the following
     * pattern:- <br>
     * 1. Examine the request<br>
     * 2. Optionally wrap the request object with a custom implementation to
     * filter content or headers for input filtering <br>
     * 3. Optionally wrap the response object with a custom implementation to
     * filter content or headers for output filtering <br>
     * 4. a) <strong>Either</strong> invoke the next entity in the chain using
     * the FilterChain object (<code>chain.doFilter()</code>), <br>
     * 4. b) <strong>or</strong> not pass on the request/response pair to the
     * next entity in the filter chain to block the request processing<br>
     * 5. Directly set headers on the response after invocation of the next
     * entity in the filter chain.
     *
     * @param request  The request to process
     * @param response The response associated with the request
     * @param chain    Provides access to the next filter in the chain for this
     *                 filter to pass the request and response to for further
     *                 processing
     * @throws IOException      if an I/O error occurs during this filter's
     *                          processing of the request
     * @throws ServletException if the processing fails for any other reason
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        WikiHttpServletRequest servletRequest = new WikiHttpServletRequest((HttpServletRequest) request);
        // 提取token
        String token = servletRequest.getHeader("X-TOKEN");
        if (StringUtil.isNotBlank(token)){
            long userId = TokenUtil.getUserIdFromToken(token,appConfig.getTokenKey());
            servletRequest.setParam("operator",String.valueOf(userId));
        }
        chain.doFilter(servletRequest,response);
    }
    public static class WikiBodyInputStream extends ServletInputStream{
        private final InputStream delegate;
        private WikiBodyInputStream(byte [] body){
            this.delegate = new ByteArrayInputStream(body);
        }

        /**
         * Has the end of this InputStream been reached?
         *
         * @return <code>true</code> if all the data has been read from the stream,
         * else <code>false</code>
         * @since Servlet 3.1
         */
        @Override
        public boolean isFinished() {
            return false;
        }

        /**
         * Can data be read from this InputStream without blocking?
         * Returns  If this method is called and returns false, the container will
         * invoke {@link ReadListener#onDataAvailable()} when data is available.
         *
         * @return <code>true</code> if data can be read without blocking, else
         * <code>false</code>
         * @since Servlet 3.1
         */
        @Override
        public boolean isReady() {
            return true;
        }

        /**
         * Sets the {@link ReadListener} for this {@link ServletInputStream} and
         * thereby switches to non-blocking IO. It is only valid to switch to
         * non-blocking IO within async processing or HTTP upgrade processing.
         *
         * @param listener The non-blocking IO read listener
         * @throws IllegalStateException If this method is called if neither
         *                               async nor HTTP upgrade is in progress or
         *                               if the {@link ReadListener} has already
         *                               been set
         * @throws NullPointerException  If listener is null
         * @since Servlet 3.1
         */
        @Override
        public void setReadListener(ReadListener listener) {

        }

        /**
         * Reads the next byte of data from the input stream. The value byte is
         * returned as an <code>int</code> in the range <code>0</code> to
         * <code>255</code>. If no byte is available because the end of the stream
         * has been reached, the value <code>-1</code> is returned. This method
         * blocks until input data is available, the end of the stream is detected,
         * or an exception is thrown.
         *
         * <p> A subclass must provide an implementation of this method.
         *
         * @return the next byte of data, or <code>-1</code> if the end of the
         * stream is reached.
         * @throws IOException if an I/O error occurs.
         */
        @Override
        public int read() throws IOException {
            return this.delegate.read();
        }
        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            return this.delegate.read(b, off, len);
        }

        @Override
        public int read(byte[] b) throws IOException {
            return this.delegate.read(b);
        }

        @Override
        public long skip(long n) throws IOException {
            return this.delegate.skip(n);
        }

        @Override
        public int available() throws IOException {
            return this.delegate.available();
        }

        @Override
        public void close() throws IOException {
            this.delegate.close();
        }

        @Override
        public synchronized void mark(int readlimit) {
            this.delegate.mark(readlimit);
        }

        @Override
        public synchronized void reset() throws IOException {
            this.delegate.reset();
        }

        @Override
        public boolean markSupported() {
            return this.delegate.markSupported();
        }
    }

    public static class WikiHttpServletRequest extends HttpServletRequestWrapper {

        private String body;
        private final ParameterMap<String,String[]> parameterMap;

        /**
         * Constructs a request object wrapping the given request.
         *
         * @param request The request to wrap
         * @throws IllegalArgumentException if the request is null
         */
        public WikiHttpServletRequest(HttpServletRequest request) {
            super(request);
            this.parameterMap = (ParameterMap<String,String[]>)request.getParameterMap();
            this.parameterMap.setLocked(false);
            readBody();
        }
        private void readBody(){
            try{
                ServletInputStream is = super.getInputStream();
                byte [] buffer = new byte[1024];
                ByteBuffer byteBuffer = new ByteBuffer(buffer.length);
                while (is.read(buffer) != -1){
                    byteBuffer.addAll(buffer);
                }
                if (byteBuffer.size() > 0){
                    body = new String(byteBuffer.array());
                }else {
                    body = "";
                }
            }catch (IOException e){
                log.error("读取");
            }
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            return new WikiBodyInputStream(body.getBytes(StandardCharsets.UTF_8));
        }

        public void setParam(String name, String value){
            if (this.getMethod().equals("GET") || this.getContentType().startsWith("multipart/form-data")){
                this.parameterMap.put(name,new String[]{value});
            }else{
                if ((!StringUtil.isNotBlank(this.getContentType()))
                        || this.getContentType().startsWith("application/x-www-form-urlencoded")){ // form
                    if (body.length() > 0){
                        body = body + "&"+name + "=" + value;
                    }else {
                        body = name + "=" + value;
                    }
                }else if (this.getContentType().startsWith("application/json")){ // json
                    Map param = null;
                    if (body.length() > 0){
                        param = JsonUtil.fromJson(body,Map.class);
                    }
                    if (param == null){
                        param = new HashMap();
                    }
                    param.put(name,value);
                    body = JsonUtil.toJson(param);
                }else {
                    // no body
                }
            }
        }
        /**
         * The default behavior of this method is to return getParameter(String
         * name) on the wrapped request object.
         */
        @Override
        public String getParameter(String name) {
            return this.parameterMap.getOrDefault(name,new String[]{null})[0];
        }

        /**
         * The default behavior of this method is to return getParameterMap() on the
         * wrapped request object.
         */
        @Override
        public Map<String, String[]> getParameterMap() {
            return this.parameterMap;
        }

        /**
         * The default behavior of this method is to return getParameterNames() on
         * the wrapped request object.
         */
        @Override
        public Enumeration<String> getParameterNames() {
            return Collections.enumeration(this.parameterMap.keySet());
        }

        /**
         * The default behavior of this method is to return
         * getParameterValues(String name) on the wrapped request object.
         */
        @Override
        public String[] getParameterValues(String name) {
            return this.parameterMap.get(name);
        }

    }
}
